﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using NinjaToolbox.SubComponentModule;

namespace NinjaToolbox.PhysicsSystem
{
    [DisallowMultipleSubComponent]
    public class PhysicsObject : MonoBehaviour, ITriggerObject
    {
        [Serializable]
        public class ErrorSettings
        {
            public float virtualRayLength = 10000f;
            public float movingError = 0.03f;
            public float movingDetectTimeSpacing = 0.1f;
            public float fallingHeadFixError = 0.001f;
        }

        [Serializable]
        public class GroundSettings
        {
            public bool enabled = true;

            public bool useFootPoint;
            public Transform footPoint;
            public Vector3 footPointRelativeOffset = new Vector3(0, -0.2f, 0);

            public LayerMask groundLayer;
            public float groundDetectLineLength = 0.5f;
        }

        [Serializable]
        public class GravitySettings
        {
            public bool enabled;

            public PhysicsObjectGravitySettingPreset preset;

            public float gravityAcce = 0.2f;
            public float gravityAcceScale = 1f;
            public float gravityAcceLimit = 10f;
            public float gravityScale = 0.5f;

            [HideInInspector]
            public float currentGravityAcce;
        }

        [Serializable]
        public class FallingHeadFixSettings
        {
            public bool enabled;
            public Vector3 fixCenterOffsetPoint = new Vector3(0, 1.5f, 0);
            public float gradient = 1f;
            public float radius = 2f;
            public float minSpeed = 100f;
            public float maxSpeed = 200f;
            public float speedAreaTopExtern = 1f;
            public float speedAreaBottomExtern = 0f;
        }

        [SerializeField]
        OBBBox mColliderShape = new OBBBox() { size = Vector3.one, gizmosColor = Color.Lerp(Color.green, Color.white, 0.6f) };
        public ErrorSettings errorSettings = new ErrorSettings();
        public GroundSettings groundSettings = new GroundSettings();
        public GravitySettings gravitySettings = new GravitySettings();
        public FallingHeadFixSettings fallingHeadFixSettings = new FallingHeadFixSettings();

        public bool isDrawGizmos = true;
        public bool alwaysVisibleGizmos = true;

        FallingHeadDetector mFallingHeadDetector;

        bool mIsOnGround;
        bool mIsMoving;
        bool mIsRising;
        bool mIsFalling;
        Vector3? mLastPosition;
        float mMovingDetectLastTick;

        RaycastHit mCurrentFrameGroundRaycastHit;

        Vector3 GroundFootPoint { get { return groundSettings.useFootPoint ? groundSettings.footPoint.position : transform.position + groundSettings.footPointRelativeOffset; } }

        public bool IsOnGround { get { return mIsOnGround; } }

        public bool IsMoving { get { return mIsMoving; } }
        public bool IsRising { get { return mIsRising; } }
        public bool IsFalling { get { return mIsFalling; } }
        public Vector3 Velocity { get { return transform.position - mLastPosition.GetValueOrDefault(transform.position); } }

        public OBBBox ColliderShape { get { mColliderShape.matrix = transform.localToWorldMatrix; return mColliderShape; } }

        public RaycastHit CurrentFrameGroundRaycastHit { get { return mCurrentFrameGroundRaycastHit; } }

        public event Action OnGroundRaycastHited;

        /// <summary>
        /// Arg1 - Self, Arg2 - Other.
        /// </summary>
        public event Action<PhysicsObject, PhysicsObject> OnTriggerEnter;

        /// <summary>
        /// Arg1 - Self, Arg2 - Other.
        /// </summary>
        public event Action<PhysicsObject, PhysicsObject> OnTriggerStay;

        /// <summary>
        /// Arg1 - Self, Arg2 - Other.
        /// </summary>
        public event Action<PhysicsObject, PhysicsObject> OnTriggerExit;


        public void SetColliderShape(OBBBox shape)
        {
            mColliderShape = shape;
        }

        void Awake()
        {
            mFallingHeadDetector = new FallingHeadDetector();
            mFallingHeadDetector.Gradient = fallingHeadFixSettings.gradient;
            mFallingHeadDetector.Radius = fallingHeadFixSettings.radius;
            mFallingHeadDetector.FixCenterOffsetPoint = fallingHeadFixSettings.fixCenterOffsetPoint;
            mFallingHeadDetector.MinSpeed = fallingHeadFixSettings.minSpeed;
            mFallingHeadDetector.MaxSpeed = fallingHeadFixSettings.maxSpeed;
            mFallingHeadDetector.SpeedAreaBottomExtern = fallingHeadFixSettings.speedAreaBottomExtern;
            mFallingHeadDetector.SpeedAreaTopExtern = fallingHeadFixSettings.speedAreaTopExtern;
            mFallingHeadDetector.Error = errorSettings.fallingHeadFixError;
            mFallingHeadDetector.Init(transform, transform);
        }

        void OnEnable()
        {
            PhysicsSystemManager.Instance.RegistTriggerObject(this, this);
        }

        void OnDisable()
        {
            if (PhysicsSystemManager.Instance != null)
                PhysicsSystemManager.Instance.UnregistTriggerObject(this);
        }

        void OnValidate()
        {
            if (groundSettings != null)
            {
                if (groundSettings.groundDetectLineLength < 0)
                    groundSettings.groundDetectLineLength = 0;
            }
        }

        void Update()
        {
            UpdateGravity();
            UpdateMovingDetect();
            UpdateFallingHeadFixDetect();

            UpdateGroundDetect();
        }

        void OnDrawGizmos()
        {
            if (!alwaysVisibleGizmos) return;

            DrawGizmos();
        }

        void OnDrawGizmosSelected()
        {
            if (alwaysVisibleGizmos) return;

            DrawGizmos();
        }

        void DrawGizmos()
        {
            if (!isDrawGizmos) return;

            DrawGroundDetectGizmos();
            DrawFallingHeadFixGizmos();
            ColliderShape.DrawGizmos();
        }

        void UpdateGravity()
        {
            if (!gravitySettings.enabled) return;
            if (mIsOnGround)
            {
                gravitySettings.currentGravityAcce = 0;
                return;
            }

            var gravityAcce = gravitySettings.gravityAcce;
            var gravityAcceScale = gravitySettings.gravityAcceScale;
            var gravityAcceLimit = gravitySettings.gravityAcceLimit;
            var gravityScale = gravitySettings.gravityScale;

            if (gravitySettings.preset != null)
            {
                gravityAcce = gravitySettings.preset.gravityAcce;
                gravityAcceScale = gravitySettings.preset.gravityAcceScale;
                gravityAcceLimit = gravitySettings.preset.gravityAcceLimit;
                gravityScale = gravitySettings.preset.gravityScale;
            }

            gravitySettings.currentGravityAcce += gravityAcce;
            gravitySettings.currentGravityAcce *= gravityAcceScale;
            gravitySettings.currentGravityAcce = Mathf.Min(gravitySettings.currentGravityAcce, gravityAcceLimit);
            var currentGravity = (gravitySettings.currentGravityAcce * Physics.gravity.normalized) + Physics.gravity * gravityScale;
            currentGravity = currentGravity * Time.deltaTime;

            var edgePosition = ColliderShape.MassPointToEdge(ColliderShape.GetWorldPosition(), ColliderShape.GetWorldPosition() + currentGravity.normalized * errorSettings.virtualRayLength);
            var relativeOffset = ColliderShape.GetWorldPosition() - edgePosition;
            var to = (edgePosition + currentGravity) - relativeOffset;
            PhysicsUtil.StepMove(this, to, new PhysicsUtil.StepMoveOptions() { StepLength = 0.1f });
        }

        void UpdateGroundDetect()
        {
            if (!groundSettings.enabled) return;

            var ray = new Ray(GroundFootPoint, -transform.up);
            var isHit = Physics.Raycast(ray, out mCurrentFrameGroundRaycastHit, groundSettings.groundDetectLineLength, groundSettings.groundLayer);
            mIsOnGround = isHit;

            if (OnGroundRaycastHited != null)
                OnGroundRaycastHited();
        }

        void UpdateMovingDetect()
        {
            if (Time.time - mMovingDetectLastTick >= errorSettings.movingDetectTimeSpacing)
            {
                mMovingDetectLastTick = Time.time;
            }
            else
            {
                return;
            }

            mIsMoving = false;
            mIsRising = false;
            mIsFalling = false;

            var delta = transform.position - mLastPosition.GetValueOrDefault(transform.position);

            if (delta.magnitude > errorSettings.movingError)
            {
                mIsMoving = true;

                var y = delta.y;
                if (y > 0)
                    mIsRising = true;
                else if (y < 0)
                    mIsFalling = true;
            }

            mLastPosition = transform.position;
        }

        void UpdateFallingHeadFixDetect()
        {
            if (!fallingHeadFixSettings.enabled) return;

            mFallingHeadDetector.Gradient = fallingHeadFixSettings.gradient;
            mFallingHeadDetector.Radius = fallingHeadFixSettings.radius;
            mFallingHeadDetector.FixCenterOffsetPoint = fallingHeadFixSettings.fixCenterOffsetPoint;
            mFallingHeadDetector.MinSpeed = fallingHeadFixSettings.minSpeed;
            mFallingHeadDetector.MaxSpeed = fallingHeadFixSettings.maxSpeed;
            mFallingHeadDetector.SpeedAreaBottomExtern = fallingHeadFixSettings.speedAreaBottomExtern;
            mFallingHeadDetector.SpeedAreaTopExtern = fallingHeadFixSettings.speedAreaTopExtern;
            mFallingHeadDetector.Error = errorSettings.fallingHeadFixError;

            mFallingHeadDetector.Update();
        }

        void DrawGroundDetectGizmos()
        {
            if (!groundSettings.enabled) return;

            var cacheGizmosColor = Gizmos.color;

            Gizmos.color = Color.blue;
            var vector = GroundFootPoint;
            vector += groundSettings.groundDetectLineLength * -transform.up;
            Gizmos.DrawLine(GroundFootPoint, vector);

            Gizmos.color = cacheGizmosColor;
        }

        void DrawFallingHeadFixGizmos()
        {
            if (!fallingHeadFixSettings.enabled) return;

            if (!Application.isPlaying)
            {
                if (mFallingHeadDetector == null)
                {
                    mFallingHeadDetector = new FallingHeadDetector();

                    mFallingHeadDetector.Init(transform, transform);
                }

                mFallingHeadDetector.Gradient = fallingHeadFixSettings.gradient;
                mFallingHeadDetector.Radius = fallingHeadFixSettings.radius;
                mFallingHeadDetector.FixCenterOffsetPoint = fallingHeadFixSettings.fixCenterOffsetPoint;
                mFallingHeadDetector.MinSpeed = fallingHeadFixSettings.minSpeed;
                mFallingHeadDetector.MaxSpeed = fallingHeadFixSettings.maxSpeed;
                mFallingHeadDetector.SpeedAreaBottomExtern = fallingHeadFixSettings.speedAreaBottomExtern;
                mFallingHeadDetector.SpeedAreaTopExtern = fallingHeadFixSettings.speedAreaTopExtern;
                mFallingHeadDetector.Error = errorSettings.fallingHeadFixError;
            }

            mFallingHeadDetector.DrawGizmos();
        }

        #region --- ITriggerObject Members ---
        void ITriggerObject.ExecuteOnTriggerEnter(PhysicsObject triggeredObject)
        {
            if (OnTriggerEnter != null)
                OnTriggerEnter(this, triggeredObject);
        }

        void ITriggerObject.ExecuteOnTriggerStay(PhysicsObject triggeredObject)
        {
            if (OnTriggerStay != null)
                OnTriggerStay(this, triggeredObject);
        }

        void ITriggerObject.ExecuteOnTriggerExit(PhysicsObject triggeredObject)
        {
            if (OnTriggerExit != null)
                OnTriggerExit(this, triggeredObject);
        }
        #endregion
    }
}
